/*
 * Copyright 2007 Matt Jensen
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.jtell.config.annotation;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jtell.EventMetadataSource;
import org.jtell.EventMetadata;
import org.jtell.internal.config.annotation.AnnotationUtils;
import org.jtell.internal.Guard;
import org.jtell.internal.ReflectionUtils;
import org.jtell.internal.EventMetadataImpl;
import org.jtell.internal.Empties;

import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;

/**
 * <p>
 * <code>AnnotationEventMetadataSource</code> is an implementation of the {@link EventMetadataSource} interface which
 * attempts to read event metadata from annotations on the event class.
 * </p>
 * <p>
 * <strong>Thread Safety</strong><br/>
 * Instances of this class are safe for multithreaded access. 
 * </p>
 */
public class AnnotationEventMetadataSource implements EventMetadataSource
{
    private static Log LOG = LogFactory.getLog(AnnotationEventMetadataSource.class);

    /**
     * <p>
     * Cache of metadata for previously encountered event classes. 
     * </p>
     */
    private final ConcurrentMap<String, EventMetadata> m_eventClassNameMetadataCache =
            new ConcurrentHashMap<String, EventMetadata>();

    /**
     * <p>
     * Construct a {@link AnnotationEventMetadataSource} instance.
     * </p>
     */
    public AnnotationEventMetadataSource()
    {
        super();
    }

    /* inherit javadoc */
    public EventMetadata getEventMetadata(final Object event)
    {
        Guard.notNull("event", event);

        // Check for metadata in cache.
        final Class<?> eventClass = ReflectionUtils.getApplicationClass(event.getClass());
        final String eventClassName = eventClass.getName();
        final EventMetadata eventMetadata;
        if (m_eventClassNameMetadataCache.containsKey(eventClassName))
        {
            // Return existing metadata from cache.
            eventMetadata = m_eventClassNameMetadataCache.get(eventClassName);
        }
        else if (!eventClass.isAnnotationPresent(Event.class))
        {
            // We only handle classes with an @Event annotation.
            eventMetadata = null;
        }
        else
        {
            // Nothing in cache, read metadata from the annotation. Attempt to store the result, but we don't care
            // whether we are successful or not in this case. If somebody else wins, they are inserting the same thing.
            final Event eventAnnotation = eventClass.getAnnotation(Event.class);
            eventMetadata = createEventMetadata(event, eventClass, eventAnnotation);
            m_eventClassNameMetadataCache.putIfAbsent(eventClassName, eventMetadata);
        }

        // Translate EMPTY_METADATA to null if necessary, then return.
        final EventMetadata result;
        if (EMPTY_METADATA == eventMetadata)
        {
            result = null;
        }
        else
        {
            result = eventMetadata;
        }
        return result;
    }

    /**
     * <p>
     * Create an event metadata object from the information in an {@link Event} annotation.
     * </p>
     *
     * @param event the event object.
     * @param eventClass the event class.
     * @param eventAnnotation the event annotation.
     * @return {@link EventMetadata} instance.
     */
    private EventMetadata createEventMetadata(final Object event,
                                              final Class<?> eventClass,
                                              final Event eventAnnotation)
    {
        // Get the attributes.
        final Map<String, String> attributes = AnnotationUtils.getAttributes(eventAnnotation);

        // Get the annotated event and source classes; skip if all attributes are empty.
        final EventMetadata result;
        final Class<?> annotatedEventClass = eventAnnotation.eventClass();
        final Class<?> annotatedSourceClass = eventAnnotation.sourceClass();
        if (attributes.isEmpty() && Void.class.equals(annotatedEventClass) &&
                Void.class.equals(annotatedSourceClass))
        {
            result = EMPTY_METADATA;
            if (LOG.isWarnEnabled())
            {
                LOG.warn(String.format("Event class [%s] has empty @Event annotation.", eventClass.getName()));
            }
        }
        else
        {
            // Get the event and source classes.
            final Class<?> metadataEventClass;
            if (Void.class.equals(annotatedEventClass))
            {
                metadataEventClass = eventClass;
            }
            else
            {
                if (!annotatedEventClass.isAssignableFrom(eventClass))
                {
                    throw new IllegalArgumentException(String.format(
                            "Event class [%s] annotated for event [%s] is not assignable from [%s].",
                            annotatedEventClass.getName(), event, eventClass.getName()));
                }
                metadataEventClass = annotatedEventClass;
            }

            // Get the source class.
            final Class<?> sourceClass;
            if (Void.class.equals(annotatedSourceClass))
            {
                sourceClass = Object.class;
            }
            else
            {
                sourceClass = annotatedSourceClass;
            }

            // Create the metadata instance.
            result = new EventMetadataImpl(metadataEventClass.getName(), sourceClass.getName(), attributes);
            if (LOG.isDebugEnabled())
            {
                LOG.debug(String.format("Returning metadata [%s] from annotation [%s] on event [%s].", result,
                        eventAnnotation, event));
            }
        }
        return result;
    }

    /**
     * <p>
     * Dummy event metadata instance which is stored in cache slots of classes which have been examined but contain no
     * metadata annotation (or an empty annotation.) 
     * </p>
     */
    private static final EventMetadata EMPTY_METADATA = new EventMetadataImpl("", "", Empties.EMPTY_ATTRIBUTES); 
}
